<?php

namespace console\controllers;

use Yii;
use yii\console\Exception;
use yii\console\ExitCode;
use yii\helpers\Console;
use yii\helpers\FileHelper;
use yii\helpers\ArrayHelper;
use yii\helpers\Inflector;
use console\db\MigrationBatchRecord;

/**
 * Manages application migrations.
 *
 * @author emhome <emhome@163.com>
 * @since 2.0
 */
class MigrateController extends \yii\console\controllers\MigrateController {

    /**
     * @var string the comment for the table being created.
     */
    const BATCH_PAGE_SIZE = 10;

    /**
     * @var string the comment for the table being created.
     */
    const BATCH_TABLE_SIZE = 128;

    /**
     * @var string the comment for the table being created.
     */
    const MIGRATE_BUILD_PREDATE = '220109';

    /**
     * @var string the comment for the table being created.
     */
    public $migrationPath = [
        '@app/migrations/found',
        '@app/migrations/init',
    ];

    /**
     * @var string the comment for the table being created.
     */
    public $batchMigrationPath = [
        '@app/migrations/batch',
    ];

    /**
     * @var string the comment for the table being created.
     */
    public $batchClasses = [
        'm300100_01' => 'BatchUserCreditAccountLogs',
    ];

    /**
     * @var string the comment for the table being created.
     */
    public $actionTime;

    /**
     * {@inheritdoc}
     */
    public function beforeAction($action) {
        $this->actionTime = microtime(true);
        return parent::beforeAction($action);
    }

    /**
     * {@inheritdoc}
     */
    public function afterAction($action, $result) {
        $data = parent::afterAction($action, $result);
        // your custom code here
        $time = microtime(true) - $this->actionTime;
        $this->stdout("execution time: " . sprintf('%.3f', $time) . "s\n\n", Console::FG_GREEN);
        return $data;
    }

    /**
     * {@inheritdoc}
     * @since 2.0.8
     */
    protected function generateMigrationCodeSchema($params) {
        $templateFile = '@app/views/createTableMigration.php';
        return $this->renderFile(Yii::getAlias($templateFile), $params);
    }

    /**
     * {@inheritdoc}
     * @since 2.0.8
     */
    public function buildMigrationFile($data, $index) {
        $name = $data['name'];
        if (!preg_match('/^[\w\\\\]+$/', $name)) {
            throw new Exception('The migration name should contain letters, digits, underscore and/or backslash characters only.');
        }
        $predate = static::MIGRATE_BUILD_PREDATE ?: date('ymd');
        $className = 'm' . $predate . '_' . gmdate('His', ($index + 1) * 10) . '_' . $name;
        // Abort if name is too long
        $nameLimit = $this->getMigrationNameLimit();
        if ($nameLimit !== null && strlen($className) > $nameLimit) {
            throw new Exception('The migration name is too long.');
        }
        $migrationPath = is_array($this->migrationPath) ? reset($this->migrationPath) : $this->migrationPath;

        $file = $migrationPath . DIRECTORY_SEPARATOR . $className . '.php';

        $content = $this->generateMigrationCodeSchema([
            'name' => $name,
            'className' => $className,
            'desc' => $name,
            'tables' => $data['tables'],
        ]);
        FileHelper::createDirectory($migrationPath);
        if (file_put_contents($file, $content, LOCK_EX) === false) {
            $this->stdout("Failed to create new migration.\n", Console::FG_RED);

            return ExitCode::IOERR;
        }

        FileHelper::changeOwnership($file, $this->newFileOwnership, $this->newFileMode);

        $this->stdout("New migration created successfully.\n", Console::FG_GREEN);
    }

    /**
     * Redoes the last few migrations.
     *
     * This command will first revert the specified migrations, and then apply
     * them again. For example,
     *
     * ```
     * yii migrate/redo     # redo the last applied migration
     * yii migrate/redo 3   # redo the last 3 applied migrations
     * yii migrate/redo all # redo all migrations
     * yii migrate/redo 101129_185401                          # using timestamp
     * yii migrate/redo m101129_185401_create_user_table       # using full name
     * yii migrate/redo app\migrations\M101129185401CreateUser # using full namespace name
     * ```
     *
     * @param int|string $limit the number of migrations to be redone. Defaults to 1,
     * meaning the last applied migration will be redone. When equals "all", all migrations will be redone.
     * @throws Exception if the number of the steps specified is less than 1.
     *
     * @return int the status of the action execution. 0 means normal, other values mean abnormal.
     */
    public function actionCopy() {
        /* @var $db \yii\db\Connection */
        $db = Yii::$app->has('buildDb') ? Yii::$app->buildDb : Yii::$app->getDb();
        /* @var $schema \yii\db\Schema */
        $schema = $db->getSchema();
        $tableSchemas = $db->createCommand('SHOW TABLE STATUS')->queryAll();
        $groupTables = [];
        foreach ($tableSchemas as $tableSchema) {
            $name = $tableSchema['Name'];
            $this->stdout($name . "\n", Console::FG_RED);
            $temp = preg_replace('/' . $db->tablePrefix . '/', '', $name, 1);
            if ($temp === 'migrations') {
                continue;
            }
            $names = explode('_', $temp);
            if (isset($groupTables[$names[0]])) {
                $groupTables[$names[0]]['tables'][] = [
                    'method' => Inflector::variablize('build_table_' . $temp),
                    'name' => $temp,
                    'comment' => $tableSchema['Comment'],
                    'tableSchema' => $schema->getTableSchema($name),
                    'tableIndexes' => $schema->getTableIndexes($name),
                ];
            } else {
                $groupTables[$names[0]] = [
                    'name' => $names[0],
                    'tables' => [
                        [
                            'method' => Inflector::variablize('build_table_' . $temp),
                            'name' => $temp,
                            'comment' => $tableSchema['Comment'],
                            'tableSchema' => $schema->getTableSchema($name),
                            'tableIndexes' => $schema->getTableIndexes($name),
                        ]
                    ],
                ];
            }
        }
        $i = 0;
        foreach ($groupTables as $groupTable) {
            $this->buildMigrationFile($groupTable, $i++);
        }
        return ExitCode::OK;
    }

    /**
     * Redoes the last few migrations.
     *
     * This command will first revert the specified migrations, and then apply
     * them again. For example,
     *
     * ```
     * yii migrate/batch user_credit_account_logs 0
     * ...
     * yii migrate/batch user_credit_account_logs 1
     * ```
     *
     * @param int|string $index the number of migrations to be redone. Defaults to 1,
     * meaning the last applied migration will be redone. When equals "all", all migrations will be redone.
     * @throws Exception if the number of the steps specified is less than 1.
     *
     * @return int the status of the action execution. 0 means normal, other values mean abnormal.
     */
    public function actionBatch($name, $index) {
        $maxIndex = ceil(static::BATCH_TABLE_SIZE / static::BATCH_PAGE_SIZE);
        if ($index >= $maxIndex) {
            $this->stdout("failed to batch apply $index (max index allow: " . $maxIndex . " )\n\n", Console::FG_RED);
            return false;
        }
        $start = $index * static::BATCH_PAGE_SIZE;
        $end = min($start + static::BATCH_PAGE_SIZE, static::BATCH_TABLE_SIZE);
        $camelizeName = Inflector::camelize('batch_' . $name);
        for ($i = $start; $i < $end; $i++) {
            if ($this->migrateBatch($camelizeName, $i) === false) {
                return false;
            }
        }
        return true;
    }

    /**
     * Redoes the last few migrations.
     *
     * This command will first revert the specified migrations, and then apply
     * them again. For example,
     *
     * ```
     * yii migrate/batch-inc user_credit_account_logs
     * ```
     *
     * @param int|string $name the number of migrations to be redone. Defaults to 1,
     * meaning the last applied migration will be redone. When equals "all", all migrations will be redone.
     * @throws Exception if the number of the steps specified is less than 1.
     *
     * @return int the status of the action execution. 0 means normal, other values mean abnormal.
     */
    public function actionBatchInc($name) {
        $camelizeName = Inflector::camelize('batch_' . $name);
        for ($i = 0; $i < static::BATCH_TABLE_SIZE; $i++) {
            if ($this->migrateBatch($camelizeName, $i, false) === false) {
                return false;
            }
        }
        return true;
    }

    protected function migrateBatch($class, $index, $beforeTruncateTable = null) {
        $start = microtime(true);
        $this->stdout("batch apply $class (part: $index) \n\n", Console::FG_YELLOW);
        $keyname = array_search($class, $this->batchClasses);
        if (!$keyname) {
            $time = microtime(true) - $start;
            $this->stdout("failed to batch apply $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_RED);
            return false;
        }

        $migration = Yii::createObject([
            'class' => "console\\migrations\\batch\\" . $class,
            'db' => $this->db,
            'compact' => $this->compact,
            'partTableIndex' => $index,
        ]);
        if ($beforeTruncateTable !== null) {
            $migration->beforeTruncateTargetTable = $beforeTruncateTable;
        }
        if ($migration->up() !== false) {
            $time = microtime(true) - $start;
            $this->stdout("*** applied $class part of $index (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_GREEN);
            return true;
        }
        $time = microtime(true) - $start;
        $this->stdout("*** failed to apply $class part of $index (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_RED);
        return false;
    }

    /**
     * Redoes the last few migrations.
     *
     * This command will first revert the specified migrations, and then apply
     * them again. For example,
     *
     * ```
     * yii migrate/inc 描述
     * ```
     *
     * @param int|string $name the number of migrations to be redone. Defaults to 1,
     * meaning the last applied migration will be redone. When equals "all", all migrations will be redone.
     * @throws Exception if the number of the steps specified is less than 1.
     *
     * @return int the status of the action execution. 0 means normal, other values mean abnormal.
     */
    public function actionInc($name) {
        $start = microtime(true);
        $this->stdout("migration batch record task info \n\n", Console::FG_YELLOW);
        $migration = new MigrationBatchRecord();
        $migration->db = $this->db;
        $migration->compact = $this->compact;
        $migration->name = $name;
        if ($migration->up() !== false) {
            $time = microtime(true) - $start;
            $this->stdout("*** migration applied (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_GREEN);
            return true;
        }
        $time = microtime(true) - $start;
        $this->stdout("*** failed to record task (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_RED);
        return false;
    }

    /**
     * Redoes the last few migrations.
     *
     * This command will first revert the specified migrations, and then apply
     * them again. For example,
     *
     * ```
     * yii migrate/redo     # redo the last applied migration
     * yii migrate/redo 3   # redo the last 3 applied migrations
     * yii migrate/redo all # redo all migrations
     * yii migrate/redo 101129_185401                          # using timestamp
     * yii migrate/redo m101129_185401_create_user_table       # using full name
     * yii migrate/redo app\migrations\M101129185401CreateUser # using full namespace name
     * ```
     *
     * @param int|string $limit the number of migrations to be redone. Defaults to 1,
     * meaning the last applied migration will be redone. When equals "all", all migrations will be redone.
     * @throws Exception if the number of the steps specified is less than 1.
     *
     * @return int the status of the action execution. 0 means normal, other values mean abnormal.
     */
    public function actionRedo($limit = 1) {
        if (preg_match('/^\\\\?([\w_]+\\\\)+m(\d{6}_?\d{6})(\D.*)?$/is', $limit, $matches)) {
            return $this->migrateRedo(trim($limit, '\\'));
        } elseif (preg_match('/^m?(\d{6}_?\d{6})(\D.*)?$/is', $limit, $matches)) {
            return $this->migrateRedo('m' . $matches[1]);
        } else {
            return parent::actionRedo($limit);
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function getMigrationHistory($limit) {
        $history = parent::getMigrationHistory($limit);
        $ignoreVersion = ArrayHelper::getValue(Yii::$app->params, 'migrate.ignore.version', []);
        $data = [];
        foreach ($history as $version => $apply_time) {
            if (in_array($version, $ignoreVersion)) {
                continue;
            }
            $data[$version] = $apply_time;
        }
        return $data;
    }

    /**
     * Returns the migrations that are not applied.
     * @return array list of new migrations
     */
    protected function getNewMigrations() {
        $migrations = parent::getNewMigrations();
        $ignoreVersion = ArrayHelper::getValue(Yii::$app->params, 'migrate.ignore.version', []);
        foreach ($migrations as $key => $version) {
            if (in_array($version, $ignoreVersion)) {
                unset($migrations[$key]);
            }
        }
        return $migrations;
    }

    /**
     * {@inheritdoc}
     */
    public function actionFinish() {
        $history = $this->getMigrationHistory(null);
        if (empty($history)) {
            $this->stdout("No migration has been done before.\n", Console::FG_YELLOW);
            return ExitCode::OK;
        }
        $migrations = [];
        foreach (array_keys($history) as $version) {
            if (strpos($version, 'm290000_000') !== false) {
                $migrations[] = $version;
            }
        }
        $n = count($migrations);
        $this->stdout("Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be reverted:\n", Console::FG_YELLOW);
        foreach ($migrations as $migration) {
            $this->stdout("\t$migration\n");
        }
        $this->stdout("\n");

        $reverted = 0;
        if ($this->confirm('Revert the above ' . ($n === 1 ? 'migration' : 'migrations') . '?')) {
            foreach ($migrations as $migration) {
                if (!$this->migrateDown($migration)) {
                    $this->stdout("\n$reverted from $n " . ($reverted === 1 ? 'migration was' : 'migrations were') . " reverted.\n", Console::FG_RED);
                    $this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);

                    return ExitCode::UNSPECIFIED_ERROR;
                }
                $reverted++;
            }
            $this->stdout("\n$n " . ($n === 1 ? 'migration was' : 'migrations were') . " reverted.\n", Console::FG_GREEN);
            $this->stdout("\nMigrated down successfully.\n", Console::FG_GREEN);
        }

        return ExitCode::OK;
    }

    /**
     * {@inheritdoc}
     */
    protected function removeTempMigrationHistory2($version) {
        $command = $this->db->createCommand();
        $command->delete($this->migrationTable, [
            'version' => $version,
        ])->execute();
    }

    /**
     * Redoes the migration.
     *
     * @param string $version the migration to be redone.
     * @throws Exception if the number of the steps specified is less than 1.
     *
     * @return int the status of the action execution. 0 means normal, other values mean abnormal.
     */
    protected function migrateRedo($version, $beforeDown = null) {
        // try migrate up
        $migrations = $this->getNewMigrations();
        $migrationName = '';
        foreach ($migrations as $migration) {
            if (strpos($migration, $version) === 0) {
                $beforeDown = false;
                $migrationName = $migration;
                break;
            }
        }
        // try migrate down
        if ($beforeDown !== false) {
            $migrations = array_keys($this->getMigrationHistory(null));
            foreach ($migrations as $migration) {
                if (strpos($migration, $version) === 0) {
                    $beforeDown = true;
                    $migrationName = $migration;
                    break;
                }
            }
        }
        if ($migrationName === '') {
            $this->stdout("\nMigration was undefined. The rest of the migrations are canceled.\n", Console::FG_RED);
            return ExitCode::UNSPECIFIED_ERROR;
        }
        if ($beforeDown === true) {
            if ($this->confirm('Redo the above migration:' . $migrationName . '?')) {
                if (!$this->migrateDown($migrationName)) {
                    $this->stdout("\nMigration down failed. The rest of the migrations are canceled.\n", Console::FG_RED);
                    return ExitCode::UNSPECIFIED_ERROR;
                }
                if (!$this->migrateUp($migrationName)) {
                    $this->stdout("\nMigration up failed. The rest of the migrations are canceled.\n", Console::FG_RED);
                    return ExitCode::UNSPECIFIED_ERROR;
                }
                $this->stdout("\nMigration was redone successfully.\n", Console::FG_GREEN);
            } else {
                $this->stdout("Already at '$migrationName'. Nothing needs to be done.\n", Console::FG_YELLOW);
            }
        } else {
            if (!$this->migrateUp($migrationName)) {
                $this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
                return ExitCode::UNSPECIFIED_ERROR;
            }
            $this->stdout("\nMigration was successfully.\n", Console::FG_GREEN);
        }
        return ExitCode::OK;
    }

}
